其他
Frida工作原理学习
本文为看雪论坛优秀文章
看雪论坛作者ID:王麻子本人
一
frida介绍
二
frida的代码结构
frida-core: Frida 核心库
frida-gum: inline-hook 框架
bindings:
frida-python: python
frida-node: Node.js
frida-qml: Qml
frida-swift: Swift
frida-tools: CLI tools
capstone: instruction disammbler
一般我们都使用js去编写frida脚本因为js的异常处理机制非常棒相比于其他语言更高效好用。
frida-core
frida-gum
Interceptor
GumInterceptor * interceptor;
GumInvocationListener * listener;
gum_init ();
interceptor = gum_interceptor_obtain ();
//GumInvocationListener*的接口
listener = g_object_new (EXAMPLE_TYPE_LISTENER, NULL);
// 开始 hook `open` 函数
gum_interceptor_begin_transaction (interceptor);
gum_interceptor_attach_listener (interceptor,
GSIZE_TO_POINTER (gum_module_find_export_by_name (NULL, "open")),
listener,
GSIZE_TO_POINTER (EXAMPLE_HOOK_OPEN));
gum_interceptor_end_transaction (interceptor);
// 测试 hook 效果
close (open ("/etc/hosts", O_RDONLY));
// 结束 hook
gum_interceptor_detach_listener (interceptor, listener);
g_object_unref (listener);
g_object_unref (interceptor);
Stalker
Interceptor.attach(addr, {
onEnter: function (args) {
this.args0 = args[0];
this.tid = Process.getCurrentThreadId();
//跟随
Stalker.follow(this.tid, {
events: {//事件
call: true,//呼叫
ret: false,//返回
exec: true,//执行
block: false,//块
compile: false//编译
},
//接收
onReceive(events){
for (const [index,value] of Stalker.parse(events)) {
console.log(index,value);
//findModuleByAddress {"name":"libc.so","base":"0x7d1f0af000","size":3178496,"path":"/apex/com.android.runtime/lib64/bionic/libc.so"}
//console.log("tuzi",Process.findModuleByAddress(0x7d1f13adb8));
}
}
// onCallSummary(summay){
//console.log("onCallSummary"+JSON.stringify(summay));
// },
});
}, onLeave: function (retval) {
Stalker.unfollow(this.tid);
}
});
Stalker的功能实现,在线程即将执行下一条指令前,先将目标指令拷贝一份到新建的内存中,然后在新的内存中对代码进行插桩,如下图所示:
这其中使用到了代码动态重编译的方法,好处是原本的代码没有被修改,因此即便代码有完整性校验也不影响,另外由于执行过程都在用户态,省去了多次中断内核切换,性能损耗也达到了可以接受的水平。由于代码的位置发生了改变,如前文 Interceptor 一样,同样要对代码进行重定位的修复。
内存监控
//C 代码
gboolean
gum_memory_access_monitor_enable (GumMemoryAccessMonitor * self,
GError ** error)
{
if (self->enabled)
return TRUE;
// ...
self->exceptor = gum_exceptor_obtain ();
gum_exceptor_add (self->exceptor, gum_memory_access_monitor_on_exception,
self);
// ...
}
//js代码
function read_write_break(){
function hook_dlopen(addr, soName, callback) {
Interceptor.attach(addr, {
onEnter: function (args) {
var soPath = args[0].readCString();
if(soPath.indexOf(soName) != -1) hook_call_constructors();
}, onLeave: function (retval) {
}
});
}
var dlopen = Module.findExportByName("libdl.so", "dlopen");
var android_dlopen_ext = Module.findExportByName("libdl.so", "android_dlopen_ext");
hook_dlopen(dlopen, "libaes.so", set_read_write_break);
hook_dlopen(android_dlopen_ext, "libaes.so", set_read_write_break);
function set_read_write_break(){
//实现一个异常回调 处理好这个异常就可以正常返回
Process.setExceptionHandler(function(details) {
console.log(JSON.stringify(details, null, 2));
console.log("lr", DebugSymbol.fromAddress(details.context.lr));
console.log("pc", DebugSymbol.fromAddress(details.context.pc));
Memory.protect(details.memory.address, Process.pointerSize, 'rwx');
console.log(Thread.backtrace(details.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n');
return true;
});
var addr = Module.findBaseAddress("libaes.so").add(0x6666);
Memory.protect(addr, 8, '---'); //修改内存页的权限
/**
* 比如有一个地址是0x12345678 我想看一下是那个代码去访问了这个地址
* 我只需要把这个内存地址置空 有函数去访问这个地址时 就会触发非法访问异常
* 比较鸡肋这种方法 这种方法会一次修改一个内存页 并且触发一次就无效了
*/
}
}
hook原理
1.注入进程
ptrace
dlopen
2.hook 目标函数
2.1 Java Hook
Static Field Hook:静态成员hook
Method Hook:函数hook
2.2 Native So Hook
GOT Hook:全局偏移表hook
SYM Hook:符号表hook
Inline Hook:函数内联hook
执行自身代码
获取敏感信息
修改返回值
etc.
补充:frida注入之后会在远端进程分配一段内存将agent拷贝过去并在目标进程中执行代码,执行完成后会 detach 目标进程,这也是为什么在 frida 先连接上目标进程后还可以用gdb/ida等调试器连接,而先gdb连接进程后 frida 就无法再次连上的原因(frida在注入时只会ptrace一下下注入完毕后就会结束ptrace所以ptrace占坑这种反调试使用spawn方式启动即可)。
hook java层
dalvik 模式
struct Method {
ClassObject* clazz; /* method所属的类 public、native等*/
u4 accessFlags; /* 访问标记 */
u2 methodIndex; //method索引
//三个size为边界值,对于native函数,这3个size均等于参数列表的size
u2 registersSize; /* ins + locals */
u2 outsSize;
u2 insSize;
const char* name;//函数名称
/*
* Method prototype descriptor string (return and argument types)
*/
DexProto prototype;
/* short-form method descriptor string */
const char* shorty;
/*
* The remaining items are not used for abstract or native methods.
* (JNI is currently hijacking "insns" as a function pointer, set
* after the first call. For internal-native this stays null.)
*/
/* the actual code */
const u2* insns; /* instructions, in memory-mapped .dex */
/* cached JNI argument and return-type hints */
int jniArgInfo;
/*
* Native method ptr; could be actual function or a JNI bridge. We
* don't currently discriminate between DalvikBridgeFunc and
* DalvikNativeFunc; the former takes an argument superset (i.e. two
* extra args) which will be ignored. If necessary we can use
* insns==NULL to detect JNI bridge vs. internal native.
*/
DalvikBridgeFunc nativeFunc;
/*
* Register map data, if available. This will point into the DEX file
* if the data was computed during pre-verification, or into the
* linear alloc area if not.
*/
const RegisterMap* registerMap;
};
…
…
…
function replaceDalvikImplementation (fn) {
if (fn === null && dalvikOriginalMethod === null) {
return;
}
//备份原来的method,
if (dalvikOriginalMethod === null) {
dalvikOriginalMethod = Memory.dup(methodId, DVM_METHOD_SIZE);
dalvikTargetMethodId = Memory.dup(methodId, DVM_METHOD_SIZE);
}
if (fn !== null) {
//自定的代码
implementation = implement(f, fn);
let argsSize = argTypes.reduce((acc, t) => (acc + t.size), 0);
if (type === INSTANCE_METHOD) {
argsSize++;
}
// 把method变成native函数
/*
* make method native (with kAccNative)
* insSize and registersSize are set to arguments size
*/
const accessFlags = (Memory.readU32(methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS)) | kAccNative) >>> 0;
const registersSize = argsSize;
const outsSize = 0;
const insSize = argsSize;
Memory.writeU32(methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS), accessFlags);
Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_REGISTERS_SIZE), registersSize);
Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_OUTS_SIZE), outsSize);
Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_INS_SIZE), insSize);
Memory.writeU32(methodId.add(DVM_METHOD_OFFSET_JNI_ARG_INFO), computeDalvikJniArgInfo(methodId));
//调用dvmUseJNIBridge为这个Method设置一个Bridge,本质上是修改结构体中的nativeFunc为自定义的implementation函数
api.dvmUseJNIBridge(methodId, implementation);
patchedMethods.add(f);
} else {
patchedMethods.delete(f);
Memory.copy(methodId, dalvikOriginalMethod, DVM_METHOD_SIZE);
implementation = null;
}
}
quick code 模式:执行 arm 汇编指令
Interpreter 模式:由解释器解释执行 Dalvik 字节码
patchMethod(methodId, {
//jnicode入口entry_point_from_jni_改为自定义的代码
'jniCode': implementation,
//修改为access_flags_为native
'accessFlags': (Memory.readU32(methodId.add(artMethodOffset.accessFlags)) | kAccNative | kAccFastNative) >>> 0,
//art_quick_generic_jni_trampoline函数的地址
'quickCode': api.artQuickGenericJniTrampoline,
//artInterpreterToCompiledCodeBridge函数地址
'interpreterCode': api.artInterpreterToCompiledCodeBridge
});
https://evilpan.com/2022/04/05/frida-internal/
https://blog.drov.com.cn/2021/04/hook.html
https://bbs.pediy.com/thread-229215.htm
https://frida.re/docs/home/
https://www.youtube.com/watch?v=uc1mbN9EJKQ
看雪ID:王麻子本人
https://bbs.pediy.com/user-home-928079.htm
# 往期推荐
2.Frida inlineHook原理分析及简单设计一款AArch64 inlineHook工具
球分享
球点赞
球在看
点击“阅读原文”,了解更多!